/*
(c) Copyrights 2007 - 2008

Original idea by by Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/

jQuery Plugin by Tzury Bar Yochay
tzury.by@gmail.com
http://evalinux.wordpress.com
http://facebook.com/profile.php?id=513676303

Project's sites:
http://code.google.com/p/js-hotkeys/
http://github.com/tzuryby/hotkeys/tree/master

License: same as jQuery license.

USAGE:
	// simple usage
	$(document).bind('keydown', 'Ctrl+c', function(){ alert('copy anyone?');});
	// special options such as disableInIput
	$(document).bind('keydown', {combi:'Ctrl+x', disableInInput: true} , function() {});
Note:
	This plugin wraps the following jQuery methods: $.fn.find, $.fn.bind and $.fn.unbind
*/


(function (jQuery){
	// keep reference to the original $.fn.bind and $.fn.unbind
	jQuery.fn.__bind__ = jQuery.fn.bind;
	jQuery.fn.__unbind__ = jQuery.fn.unbind;
	jQuery.fn.__find__ = jQuery.fn.find;
	var hotkeys = {
		version: '0.7.8',
		override: /keydown|keypress|keyup/g,
		triggersMap: {},
		specialKeys: { 27: 'esc', 9: 'tab', 32:'space', 13: 'return', 8:'backspace', 145: 'scroll',
			20: 'capslock', 144: 'numlock', 19:'pause', 45:'insert', 36:'home', 46:'del',
			35:'end', 33: 'pageup', 34:'pagedown', 37:'left', 38:'up', 39:'right',40:'down',
			112:'f1',113:'f2', 114:'f3', 115:'f4', 116:'f5', 117:'f6', 118:'f7', 119:'f8',
			120:'f9', 121:'f10', 122:'f11', 123:'f12' },
		shiftNums: { "`":"~", "1":"!", "2":"@", "3":"#", "4":"$", "5":"%", "6":"^", "7":"&",
			"8":"*", "9":"(", "0":")", "-":"_", "=":"+", ";":":", "'":"\"", ",":"<",
			".":">",  "/":"?",  "\\":"|" },
		newTrigger: function (type, combi, callback) {
			// i.e. {'keyup': {'ctrl': {cb: callback, disableInInput: false}}}
			var result = {};
			result[type] = {};
			result[type][combi] = {cb: callback, disableInInput: false};
			return result;
		}
	};
	// add firefox num pad char codes
	if (jQuery.browser.mozilla){
		hotkeys.specialKeys = jQuery.extend(hotkeys.specialKeys, { 96: '0', 97:'1', 98: '2', 99:
			'3', 100: '4', 101: '5', 102: '6', 103: '7', 104: '8', 105: '9' });
	}
	// a wrapper around of $.fn.find
	// see more at: http://groups.google.com/group/jquery-en/browse_thread/thread/18f9825e8d22f18d
	jQuery.fn.find = function( selector ) {
		this.query=selector;
		return jQuery.fn.__find__.apply(this, arguments);
	};
	jQuery.fn.unbind = function (type, combi, fn){
		if (jQuery.isFunction(combi)){
			fn = combi;
			combi = null;
		}
		if (combi && typeof combi === 'string'){
			var selectorId = ((this.prevObject && this.prevObject.query) || (this[0].id && this[0].id) || this[0]).toString();
			var hkTypes = type.split(' ');
			for (var x=0; x<hkTypes.length; x++){
				delete hotkeys.triggersMap[selectorId][hkTypes[x]][combi];
			}
		}
		// call jQuery original unbind
		return  this.__unbind__(type, fn);
	};
	jQuery.fn.bind = function(type, data, fn){
		// grab keyup,keydown,keypress
		var handle = type.match(hotkeys.override);
		if (jQuery.isFunction(data) || !handle){
			// call jQuery.bind only
			return this.__bind__(type, data, fn);
		}
		else{
			// split the job
			var result = null,
			// pass the rest to the original $.fn.bind
			pass2jq = jQuery.trim(type.replace(hotkeys.override, ''));
			// see if there are other types, pass them to the original $.fn.bind
			if (pass2jq){
				// call original jQuery.bind()
				result = this.__bind__(pass2jq, data, fn);
			}
			if (typeof data === "string"){
				data = {'combi': data};
			}
			if(data.combi){
				for (var x=0; x < handle.length; x++){
					var eventType = handle[x];
					var combi = data.combi.toLowerCase(),
						trigger = hotkeys.newTrigger(eventType, combi, fn),
						selectorId = ((this.prevObject && this.prevObject.query) || (this[0].id && this[0].id) || this[0]).toString();
					//trigger[eventType][combi].propagate = data.propagate;
					trigger[eventType][combi].disableInInput = data.disableInInput;
					// first time selector is bounded
					if (!hotkeys.triggersMap[selectorId]) {
						hotkeys.triggersMap[selectorId] = trigger;
					}
					// first time selector is bounded with this type
					else if (!hotkeys.triggersMap[selectorId][eventType]) {
						hotkeys.triggersMap[selectorId][eventType] = trigger[eventType];
					}
					// make trigger point as array so more than one handler can be bound
					var mapPoint = hotkeys.triggersMap[selectorId][eventType][combi];
					if (!mapPoint){
						hotkeys.triggersMap[selectorId][eventType][combi] = [trigger[eventType][combi]];
					}
					else if (mapPoint.constructor !== Array){
						hotkeys.triggersMap[selectorId][eventType][combi] = [mapPoint];
					}
					else {
						hotkeys.triggersMap[selectorId][eventType][combi][mapPoint.length] = trigger[eventType][combi];
					}
					// add attribute and call $.event.add per matched element
					this.each(function(){
						// jQuery wrapper for the current element
						var jqElem = jQuery(this);
						// element already associated with another collection
						if (jqElem.attr('hkId') && jqElem.attr('hkId') !== selectorId){
							selectorId = jqElem.attr('hkId') + ";" + selectorId;
						}
						jqElem.attr('hkId', selectorId);
					});
					result = this.__bind__(handle.join(' '), data, hotkeys.handler)
				}
			}
			return result;
		}
	};
	// work-around for opera and safari where (sometimes) the target is the element which was last
	// clicked with the mouse and not the document event it would make sense to get the document
	hotkeys.findElement = function (elem){
		if (!jQuery(elem).attr('hkId')){
			if (jQuery.browser.opera || jQuery.browser.safari){
				while (!jQuery(elem).attr('hkId') && elem.parentNode){
					elem = elem.parentNode;
				}
			}
		}
		return elem;
	};
	// the event handler
	hotkeys.handler = function(event) {
		var target = hotkeys.findElement(event.currentTarget),
			jTarget = jQuery(target),
			ids = jTarget.attr('hkId');
		if(ids){
			ids = ids.split(';');
			var code = event.which,
				type = event.type,
				special = hotkeys.specialKeys[code],
				// prevent f5 overlapping with 't' (or f4 with 's', etc.)
				character = !special && String.fromCharCode(code).toLowerCase(),
				shift = event.shiftKey,
				ctrl = event.ctrlKey,
				// patch for jquery 1.2.5 && 1.2.6 see more at:
				// http://groups.google.com/group/jquery-en/browse_thread/thread/83e10b3bb1f1c32b
				alt = event.altKey || event.originalEvent.altKey,
				mapPoint = null;

			for (var x=0; x < ids.length; x++){
				if (hotkeys.triggersMap[ids[x]][type]){
					mapPoint = hotkeys.triggersMap[ids[x]][type];
					break;
				}
			}
			//find by: id.type.combi.options
			if (mapPoint){
				var trigger;
				// event type is associated with the hkId
				if(!shift && !ctrl && !alt) { // No Modifiers
					trigger = mapPoint[special] ||  (character && mapPoint[character]);
				}
				else{
					// check combinations (alt|ctrl|shift+anything)
					var modif = '';
					if(alt) modif +='alt+';
					if(ctrl) modif+= 'ctrl+';
					if(shift) modif += 'shift+';
					// modifiers + special keys or modifiers + character or modifiers + shift character or just shift character
					trigger = mapPoint[modif+special];
					if (!trigger){
						if (character){
							trigger = mapPoint[modif+character]
								|| mapPoint[modif+hotkeys.shiftNums[character]]
								// '$' can be triggered as 'Shift+4' or 'Shift+$' or just '$'
								|| (modif === 'shift+' && mapPoint[hotkeys.shiftNums[character]]);
						}
					}
				}
				if (trigger){
					var result = false;
					for (var x=0; x < trigger.length; x++){
						if(trigger[x].disableInInput){
							// double check event.currentTarget and event.target
							var elem = jQuery(event.target);
							if (jTarget.is("input") || jTarget.is("textarea")
								|| elem.is("input") || elem.is("textarea")) {
								return true;
							}
						}
						// call the registered callback function
						result = result || trigger[x].cb.apply(this, [event]);
					}
					return result;
				}
			}
		}
	};
	// place it under window so it can be extended and overridden by others
	window.hotkeys = hotkeys;
	return jQuery;
})(jQuery);
